iT邦幫忙

2023 iThome 鐵人賽

DAY 19
1
Modern Web

深入淺出,完整認識 Next.js 13 !系列 第 19

Day 19 - Next.js 13 App Router 動態路由 Dynamic Routes & getStaticParams()

  • 分享至 

  • xImage
  •  

Day 17 我們介紹了 App Router 的基本路由定義方式,簡單來說,當 /app 中的資料夾裡有 page.tsx 時,Next.js 就會以該資料夾名稱建立一個 route segment,page.tsx 裡 export default 的 component 即會是該 route segment 的 UI。假如還不熟悉的讀者可以先閱讀 Day 17 的文章。

但開發時可能會碰到一種狀況:
我們無法事先確認 route segment 的名稱,或是同一層類似的 route segment 可能有多個,不太可能一個一個建立資料夾。

以電商網站舉例,我希望 /products 後可以加入一個 id 的 segment,每頁的排版、互動邏輯都差不多,主要差在商品資訊 api 的 endpoint ( id )。假如商城裡有 10,000 個產品,很顯然建 app/products/1 ~ app/products/10000 不是個明智的選項,那該怎麼辦呢?

這時就可以使用動態路由 ( Dynamic Routes )!

動態路由 Dynamic Routes

使用方法很簡單,只需要在 dynamic segment 的資料夾名稱加上中括號 []。以上面的範例來說,dynamic segment 是 id,因此我們將資料夾名稱取為 [id],結構會長這樣:

app/
├─ products/
│  ├─ [id]/
│  │  ├─ page.tsx

那要怎麼取得 dynamic segment 的值呢? Next 會透過 params 這個 prop 將 dynamic segments 傳到 Page component 中。以上述例子來說,id 就會是 page.tsx 中的 params.id

/* /products/[id] */
export default function Products({ params }: { params: { id: string } }) {
return <div>Product ID: {params.id}</div>;
}

所以當你進到 /products/188 時,params.id 就會是 188;/products/256 時會是 256,以此類推。

取得所有 Dynamic Segments

假如 dynamic segments 不只一層,比方說 /books/fiction/sci-fi,其中 /fiction 和 /sci-fi 是 dynamic routes,那我就一定要建兩個 dynamic segments 嗎?例如:

app/
├─ books/
│  ├─ [category1]/
│  │  ├─ [category2]/
│  │  │  ├─ page.tsx

假如覺得 [category1][category2] 重複性太高,只想建一個 dynamic segment [category],但同時要能 catch 到 /fiction 和 /sci-fi,甚至 /sci-fi 子層的 dynamic segments,這時可以使用 catch-all 的方式來取得 children segments 中所有的 dynamic segments。

要使用 catch-all,只需要將資料夾名稱改為 [...資料夾名稱] 就好。

以上述圖書的例子,就可以將資料夾結構改為:

app/
├─ books/
│  ├─ [...category]/
│  │  ├─ page.tsx

catch-all 的 dynamic segments 會是一個 array,比方說進到 /books/fiction/sci-fi 時,params.category 就會是 [‘fiction’, ‘sci-fi’]

/* /books/[...category] */
export default function Books({ params }: { params: { category: string[] } }) {
console.log(params); // { category: ['fiction', 'sci-fi'] }
}

但這時你可能會發現,不管是單層的 dynamic segment 或是使用 catch-all,假如網址沒有輸入對應 [] 的 segment,就會找不到頁面。 以前面兩個案例為例,進到 /products 和 /books 就會顯示 404。

怎麼解決呢?假如是單層的 dynamic segment,那我們需要在 dynamic segment 這層也建一個 page.tsx,負責子層沒有 route segment 時的 UI:

app/
├─ product/
│  ├─ [id]/
│  │  ├─ page.tsx
│  ├─ page.tsx

當使用者進入 /product 時,就會顯示 /product/page.tsx 中的內容。那假如子層有不只一層 dynamic segments,我們又想使用 catch-all 呢?

Optional Catch-All Segments

假如有多層的 dynamic segments,例如書城的案例,想使用 catch-all 同時也希望 /books 能讀到頁面,這時我們可以使用 optional catch-all segments!

方法很簡單,我們只需要將資料夾名稱的中括號 [] 改成雙中括號 [[]],catch-all segments 就會變成 optional!以書城的案例來說,我們可以把 [...category] 改成 [[...category]]

app/
├─ books/
│  ├─ [[...category]]/
│  │  ├─ page.tsx

這時 /books/fiction/sci-fi 的 params 一樣是 [‘fiction’, ‘sci-fi’],而 /books 也會被 catch 到,它的 params 會是一個空的 object {}。假如還是不太理解,可以參考下圖官方提供的範例:
https://ithelp.ithome.com.tw/upload/images/20230919/20161853cxJDt2gOHs.png
( 圖片來源:https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes )

Build Time 生成 Dynamic Segments

Dynamic route segment 的 params 預設會在 server 每次收到 client request 時生成,假如效能考量,希望 params 在 build time 靜態生成,可以使用官方提供的 generateStaticParams() function,讓 params 在 build time 生成。我們來看個範例:

/* /product/[id]/page.tsx */
export function generateStaticParams() {
return [{ id: '1' }, { id: '2' }, { id: '3' }];
}


export default function Page({ params }: { params: { id: string } }) {
return <div>Product ID: {params.id}</div>;
}

這時 /product/1、/product/2、/product/3 的 params 就會在 build time 時生成,其他 id 的 params 則會在 run time server 收到 request 後生成。

假如我們希望使用者進到非 static params 的頁面時 ( 以上述例子,1,2,3 以外的 id ),會回傳 404 error 呢?我們可以調整 route segment 的設定,將 dynamicParams 設定為 false ( 預設是 true ):

/* /product/[id]/page.tsx */
export const dynamicParams = false;


export function generateStaticParams() {
return [{ id: '1' }, { id: '2' }, { id: '3' }];
}


export default function Page({ params }: { params: { id: string } }) {
return <div>Product ID: {params.id}</div>;
}

調整完後,我們 npm run dev 到 /product/99 看一下會不會顯示 404。結果沒有,網頁還是正常顯示且 params.id 也有讀到,怎麼會這樣?

不用慌張!那是因為在 dev mode 時,每次切換 route generateStaticParams() 都會被呼叫,所以就算設定 dynamicParams 為 false,依然還是讀得到 id。

假如網頁部署後 ( 超連結網頁我是用 Vercel 部署服務 ),/product/1、/product/2、/product/3 會有內容,但到 /product/99,這時就會顯示 404。

除了 dynamicParams 外,Route Segment 還有其他設定可以自訂,因為時間的關係就先不介紹,有興趣的朋友可以參考官方文件

generateStaticparams() 生成的 dynamic segments 也可以不只一層:

/* /product/[category]/[id]/page.tsx */
export function generateStaticParams() {
  return [
    { category: 'men', id: '1' },
    { category: 'women', id: '2' },
    { category: 'accessories', id: '3' },
  ];
}

export default function Page({
  params,
}: {
  params: { category: string; id: string };
}) {
  return (
    <div>
      Category:{params.category} Product ID: {params.id}
    </div>
  );
}

也可以使用 catch-all,或是根據 API response 設定 id,如官方範例:

/* app/blog/[slug]/page.tsx */
// Return a list of `params` to populate the [slug] dynamic segment
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}
 
// Multiple versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
export default function Page({ params }) {
  const { slug } = params
  // ...
}

以上就是動態路由的使用方法。學完基本和動態的路由定義後,下一步會帶大家來認識如何在 App Router 中切換路由。這部分就留到明天分享囉!

謝謝大家耐心的閱讀,我們明天見!


上一篇
Day 18 - Next.js 13 App Router 跨路由共用 UI:Layout 與 Template
下一篇
Day 20 - Next.js 13 App Router 路由切換:<Link> & useRouter
系列文
深入淺出,完整認識 Next.js 13 !30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言